import csv
import os
import tkinter as tk
from datetime import datetime
from tkinter import ttk, messagebox, filedialog
import numpy as np
import pandas as pd
from itertools import product
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg


class DOEFullFactorialDesigner:
    def __init__(self, parent, csv_path=None, factor_count=None, on_csv_saved=None):
        self.root = tk.Frame(parent)
        self.root.pack(fill=tk.BOTH, expand=True)
        self.csv_path = csv_path
        self.factor_count = factor_count
        self.loaded_parameters = None
        self.on_csv_saved = on_csv_saved

        # 设置中文字体
        plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

        # 创建主框架
        self.main_frame = ttk.Frame(self.root, padding="10")
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        # 创建输入区域
        self.input_frame = ttk.LabelFrame(self.main_frame, text="实验参数设置", padding="10")
        self.input_frame.pack(fill=tk.X, pady=5)

        # 因子数量设置
        ttk.Label(self.input_frame, text="因子数量:").grid(row=0, column=0, sticky=tk.W, pady=5)
        if factor_count:
            self.factor_var = tk.IntVar(value=self.factor_count)
        else:
            self.factor_var = tk.IntVar(value=2)
        self.factor_spinbox = ttk.Spinbox(self.input_frame, from_=2, to=10, textvariable=self.factor_var, width=5)
        self.factor_spinbox.grid(row=0, column=1, sticky=tk.W, pady=5)
        self.factor_spinbox.bind("<FocusOut>", self.update_alpha_value)

        # 追踪因子数量变化，确保任何方式的修改都能触发α值更新
        self.factor_var.trace_add("write", self.handle_factor_change)

        ttk.Button(self.input_frame, text="更新因子设置", command=self.update_factors_frame).grid(
            row=0, column=2, padx=5, pady=5)

        # 响应值设置 - 固定为"响应值"
        ttk.Label(self.input_frame, text="响应值名称:").grid(row=0, column=3, sticky=tk.W, pady=5)
        self.response_name_var = tk.StringVar(value="响应值")  # 固定响应值名称为"响应值"
        response_entry = ttk.Entry(self.input_frame, textvariable=self.response_name_var, width=15)
        response_entry.grid(row=0, column=4, sticky=tk.W, pady=5)
        response_entry.configure(state="readonly")  # 设置为只读，确保始终为"响应值"

        # 轴点设置
        ttk.Label(self.input_frame, text="轴点距离(α):").grid(row=0, column=5, sticky=tk.W, pady=5)
        # 根据因子数量初始化正确的α值，无论是用户输入还是其他模块传入
        if factor_count == 3:
            self.alpha_var = tk.DoubleVar(value=1.682)
        else:
            self.alpha_var = tk.DoubleVar(value=1.414)
        self.alpha_entry = ttk.Entry(self.input_frame, textvariable=self.alpha_var, width=8)
        self.alpha_entry.grid(row=0, column=6, sticky=tk.W, pady=5)
        self.alpha_entry.bind("<FocusIn>", self.on_alpha_focus_in)  # 绑定焦点进入事件

        ttk.Label(self.input_frame, text="中心点数:").grid(row=0, column=7, sticky=tk.W, pady=5)
        self.center_points_var = tk.IntVar(value=3)
        ttk.Spinbox(self.input_frame, from_=1, to=10, textvariable=self.center_points_var, width=5).grid(
            row=0, column=8, sticky=tk.W, pady=5)

        # 因子设置框架
        self.factors_frame = ttk.LabelFrame(self.main_frame, text="因子详情设置", padding="10")
        self.factors_frame.pack(fill=tk.X, pady=5)

        # 结果框架
        self.results_frame = ttk.LabelFrame(self.main_frame, text="实验设计结果", padding="10")
        self.results_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        # 结果表格
        self.tree = ttk.Treeview(self.results_frame)
        self.scrollbar_y = ttk.Scrollbar(self.results_frame, orient="vertical", command=self.tree.yview)
        self.scrollbar_x = ttk.Scrollbar(self.results_frame, orient="horizontal", command=self.tree.xview)
        self.tree.configure(yscrollcommand=self.scrollbar_y.set, xscrollcommand=self.scrollbar_x.set)
        self.scrollbar_y.pack(side=tk.RIGHT, fill=tk.Y)
        self.scrollbar_x.pack(side=tk.BOTTOM, fill=tk.X)
        self.tree.pack(fill=tk.BOTH, expand=True)

        # 按钮框架
        self.button_frame = ttk.Frame(self.main_frame)
        self.button_frame.pack(fill=tk.X, pady=5)

        ttk.Button(self.button_frame, text="生成实验设计", command=self.generate_design).pack(side=tk.LEFT, padx=5)
        ttk.Button(self.button_frame, text="添加CCC轴点", command=self.add_ccc_points).pack(side=tk.LEFT, padx=5)
        ttk.Button(self.button_frame, text="保存为CSV", command=self.save_to_csv).pack(side=tk.LEFT, padx=5)
        ttk.Button(self.button_frame, text="导入CSV", command=self.import_from_csv).pack(side=tk.LEFT, padx=5)

        # 初始化因子设置
        self.factor_entries = []
        self.update_factors_frame(self.csv_path)  # 初始化因子框架

        # 用于编辑响应值的变量
        self.editing_cell = None
        self.entry = None

        # 记录实验类型
        self.experiment_type = "full_factorial"

        # 标记用户是否手动修改了α值
        self.alpha_manually_modified = False

    def handle_factor_change(self, *args):
        """当因子数量变化时自动更新α值和因子设置"""
        self.update_alpha_value()
        self.update_factors_frame()

    def update_alpha_value(self, event=None):
        """根据因子数量更新α值，确保3个因子对应1.682，2个因子对应1.414"""
        # 如果用户已经手动修改了α值，则不自动更新
        if self.alpha_manually_modified:
            return

        factor_count = self.factor_var.get()
        # 明确设置：3个因子时α=1.682，2个因子时α=1.414
        if factor_count == 3:
            self.alpha_var.set(1.682)
        elif factor_count == 2:
            self.alpha_var.set(1.414)

    def on_alpha_focus_in(self, event=None):
        """当用户聚焦到α输入框时，标记为已手动修改"""
        self.alpha_manually_modified = True

    def _load_parameters_from_csv(self, csv_path):
        try:
            with open(csv_path, 'r', encoding='utf-8') as f:
                reader = csv.reader(f)
                headers = next(reader)  # 跳过表头
                self.loaded_parameters = [row for row in reader if row]  # 保存为实例变量，排除空行
        except Exception as e:
            messagebox.showerror("错误", f"加载参数失败: {str(e)}")
            self.loaded_parameters = []

    def update_factors_frame(self, csv_path=None):
        if csv_path:
            self._load_parameters_from_csv(csv_path)
        else:
            self.loaded_parameters = []

        """更新因子设置区域，根据因子类型显示不同的输入框"""
        # 清除现有内容
        for widget in self.factors_frame.winfo_children():
            widget.destroy()

        # 创建列标题
        headers = ["因子名称", "单位", "因子类型", "水平数", "水平设置"]
        for col, header in enumerate(headers):
            ttk.Label(self.factors_frame, text=header, font=('黑体', 10, 'bold')).grid(
                row=0, column=col, padx=10, pady=5, sticky=tk.W)

        # 获取因子数量
        if self.loaded_parameters:
            num_factors = len(self.loaded_parameters)
        else:
            num_factors = self.factor_var.get()
        self.factor_entries = []

        # 创建因子设置输入
        for i in range(num_factors):
            row = i + 1  # 从1开始，0是标题行

            # 提取CSV数据
            if self.loaded_parameters and i < len(self.loaded_parameters):
                factor, level_range, unit = self.loaded_parameters[i]
                print(level_range)
            else:
                factor, level_range, unit = f"因子{i + 1}", "", ""

            # 1. 因子名称
            name_var = tk.StringVar(value=factor)
            name_entry = ttk.Entry(self.factors_frame, textvariable=name_var, width=15)
            name_entry.grid(row=row, column=0, padx=10, pady=5, sticky=tk.W)

            # 2. 单位
            unit_var = tk.StringVar(value=unit)
            unit_entry = ttk.Entry(self.factors_frame, textvariable=unit_var, width=8)
            unit_entry.grid(row=row, column=1, padx=10, pady=5, sticky=tk.W)

            # 3. 因子类型
            type_var = tk.StringVar(value="连续型")
            type_combo = ttk.Combobox(self.factors_frame, textvariable=type_var,
                                      values=["连续型", "离散型", "分类型"],
                                      state="readonly", width=8)
            type_combo.grid(row=row, column=2, padx=10, pady=5, sticky=tk.W)

            # 4. 水平数
            level_var = tk.IntVar(value=2)
            level_spin = ttk.Spinbox(self.factors_frame, from_=2, to=10, textvariable=level_var, width=5)
            level_spin.grid(row=row, column=3, padx=10, pady=5, sticky=tk.W)

            # 5. 水平设置区域（根据类型显示不同输入框）
            setting_frame = ttk.Frame(self.factors_frame)
            setting_frame.grid(row=row, column=4, padx=10, pady=5, sticky=tk.W)

            # 为每个因子创建独立的变量和输入框，避免引用冲突
            def create_input_widgets(frame, min_val="0", max_val="1"):
                # 连续型输入框（最大值和最小值）
                min_var = tk.StringVar(value=min_val)
                max_var = tk.StringVar(value=max_val)

                # 离散型/分类型输入框（单一逗号分隔框）
                values_var = tk.StringVar(value="")

                return {
                    'min_var': min_var,
                    'max_var': max_var,
                    'values_var': values_var
                }

            min_val, max_val = "0", "1"
            if "-" in level_range:
                try:
                    parts = level_range.split("-")
                    min_val = parts[0].strip()
                    max_val = parts[1].strip()
                except Exception as e:
                    print(f"解析失败: {level_range} -> {e}")

            # 创建输入框变量，并初始化 min/max 值
            input_vars = create_input_widgets(setting_frame, min_val=min_val, max_val=max_val)

            # 根据因子类型显示/隐藏输入框的函数
            def toggle_fields(event=None, type_var=type_var, frame=setting_frame, vars=input_vars):
                factor_type = type_var.get()
                # 清除当前显示的控件
                for widget in frame.winfo_children():
                    widget.destroy()

                if factor_type == "连续型":
                    # 显示最大值和最小值输入框
                    ttk.Label(frame, text="最小值:").grid(row=0, column=0, sticky=tk.W, padx=2)
                    ttk.Entry(frame, textvariable=vars['min_var'], width=10).grid(row=0, column=1, padx=2)
                    ttk.Label(frame, text="最大值:").grid(row=0, column=2, sticky=tk.W, padx=2)
                    ttk.Entry(frame, textvariable=vars['max_var'], width=10).grid(row=0, column=3, padx=2)
                else:
                    # 显示单一逗号分隔输入框（离散型和分类型）
                    ttk.Label(frame, text="水平值(逗号分隔):").grid(row=0, column=0, sticky=tk.W, padx=2)
                    ttk.Entry(frame, textvariable=vars['values_var'], width=30).grid(row=0, column=1, padx=2)
                    if factor_type == "离散型":
                        ttk.Label(frame, text="例如:10,20,30").grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=2)
                    else:
                        ttk.Label(frame, text="例如:A,B,C").grid(row=1, column=0, columnspan=2, sticky=tk.W, padx=2)

            # 绑定类型变化事件
            type_combo.bind("<<ComboboxSelected>>", toggle_fields)

            # 初始显示
            toggle_fields()

            # 保存因子设置
            self.factor_entries.append({
                'name': name_var,
                'unit': unit_var,
                'type': type_var,
                'level_count': level_var,
                'min_value': input_vars['min_var'],
                'max_value': input_vars['max_var'],
                'values': input_vars['values_var']
            })

    def generate_design(self):
        """生成全因子实验设计"""
        try:
            self.factor_details = []
            for factor in self.factor_entries:
                name = factor['name'].get().strip()
                if not name:
                    raise ValueError("因子名称不能为空")

                unit = factor['unit'].get().strip()
                factor_type = factor['type'].get()
                level_count = factor['level_count'].get()

                # 处理水平值
                if factor_type == "连续型":
                    # 连续型：处理最大值和最小值
                    try:
                        min_val = float(factor['min_value'].get())
                        max_val = float(factor['max_value'].get())
                    except ValueError:
                        raise ValueError(f"因子 {name} 的最小值和最大值必须为数字")

                    if min_val >= max_val:
                        raise ValueError(f"因子 {name} 的最小值必须小于最大值")

                    # 生成均匀分布的水平值
                    level_values = np.linspace(min_val, max_val, level_count).tolist()
                    level_values = [round(v, 2) for v in level_values]

                else:  # 离散型或分类型
                    # 处理逗号分隔的水平值
                    values_str = factor['values'].get().strip()
                    if not values_str:
                        raise ValueError(f"因子 {name} 的水平值不能为空")

                    # 分割并去除空格
                    level_values = [v.strip() for v in values_str.split(',')]

                    # 检查水平数量是否匹配
                    if len(level_values) != level_count:
                        raise ValueError(
                            f"因子 {name} 的水平数量不匹配（输入: {len(level_values)}, 设置: {level_count}）")

                    # 离散型转换为数值
                    if factor_type == "离散型":
                        try:
                            level_values = [float(v) for v in level_values]
                        except ValueError:
                            raise ValueError(f"因子 {name} 为离散型，水平值必须为数字")

                self.factor_details.append({
                    'name': name,
                    'unit': unit,
                    'type': factor_type,
                    'levels': level_values,
                    'min': min_val if factor_type == "连续型" else level_values[0],
                    'max': max_val if factor_type == "连续型" else level_values[-1]
                })

            # 生成全因子组合
            factor_names = [f"{f['name']}({f['unit']})" if f['unit'] else f['name'] for f in self.factor_details]
            level_combinations = list(product(*[f['levels'] for f in self.factor_details]))

            # 创建数据框
            df = pd.DataFrame(level_combinations, columns=factor_names)
            df.index.name = "实验编号"
            df.reset_index(inplace=True)
            df["实验编号"] += 1  # 实验编号从1开始

            # 添加响应值列，固定为"响应值"
            df["响应值"] = ""

            # 显示结果
            total = len(df)
            messagebox.showinfo("成功", f"已生成 {total} 组全因子实验设计")
            self.display_results(df)
            self.design_df = df
            self.experiment_type = "full_factorial"

            # 绑定点击事件，使响应值可编辑
            self.tree.bind("<Double-1>", self.on_double_click)

        except Exception as e:
            messagebox.showerror("错误", str(e))

    def add_ccc_points(self):
        """在现有全因子设计的基础上添加CCC中心复合设计的轴点"""
        if not hasattr(self, 'design_df') or self.experiment_type != "full_factorial":
            messagebox.showerror("错误", "请先生成全因子实验设计")
            return

        try:
            # 获取轴点距离(α)和中心点数
            alpha = self.alpha_var.get()
            if alpha <= 0:
                raise ValueError("轴点距离(α)必须大于0")

            center_points = self.center_points_var.get()
            if center_points < 1:
                raise ValueError("中心点数必须至少为1")

            # 获取因子信息
            factor_names = [f"{f['name']}({f['unit']})" if f['unit'] else f['name'] for f in self.factor_details]

            # 计算每个因子的中心点、低水平和高水平
            factor_info = {}
            for i, factor in enumerate(self.factor_details):
                name = factor_names[i]
                if factor['type'] == "连续型":
                    center = (factor['min'] + factor['max']) / 2
                    delta = (factor['max'] - factor['min']) / 2
                    factor_info[name] = {
                        'center': center,
                        'low': factor['min'],
                        'high': factor['max'],
                        'delta': delta
                    }
                else:
                    # 对于离散型和分类型因子，不添加轴点
                    messagebox.showwarning("警告", f"因子 {factor['name']} 为非连续型，不添加轴点")
                    return

            # 创建轴点
            axial_points = []
            num_factors = len(self.factor_details)

            # 为每个因子创建两个轴点(+α和-α)
            for i, factor in enumerate(self.factor_details):
                name = factor_names[i]
                if factor['type'] == "连续型":
                    # 创建中心点的基础值
                    center_point = [factor_info[fn]['center'] for fn in factor_names]

                    # +α轴点
                    pos_point = center_point.copy()
                    pos_point[i] = factor_info[name]['center'] + alpha * factor_info[name]['delta']
                    axial_points.append(pos_point)

                    # -α轴点
                    neg_point = center_point.copy()
                    neg_point[i] = factor_info[name]['center'] - alpha * factor_info[name]['delta']
                    axial_points.append(neg_point)

            # 创建中心点
            center_point = [factor_info[fn]['center'] for fn in factor_names]
            for _ in range(center_points):
                axial_points.append(center_point.copy())

            # 创建轴点的数据框
            axial_df = pd.DataFrame(axial_points, columns=factor_names)
            axial_df.index.name = "实验编号"
            axial_df.reset_index(inplace=True)

            # 计算新的实验编号
            last_id = self.design_df['实验编号'].max()
            axial_df['实验编号'] = [last_id + i + 1 for i in range(len(axial_df))]

            # 添加响应值列，固定为"响应值"
            axial_df["响应值"] = ""

            # 合并全因子设计和轴点设计
            self.design_df = pd.concat([self.design_df, axial_df], ignore_index=True)

            # 更新显示
            self.display_results(self.design_df)
            self.experiment_type = "ccc"

            messagebox.showinfo("成功", f"已添加 {len(axial_df)} 个CCC轴点，当前α值: {alpha}")

        except Exception as e:
            messagebox.showerror("错误", f"添加CCC轴点失败: {str(e)}")

    def display_results(self, df):
        """在表格中显示实验设计结果"""
        # 清除现有数据
        for item in self.tree.get_children():
            self.tree.delete(item)

        # 设置列
        self.tree["columns"] = list(df.columns)
        self.tree["show"] = "headings"

        # 设置列标题和宽度
        for col in df.columns:
            self.tree.heading(col, text=col)
            max_width = max(len(str(x)) for x in df[col]) * 10
            self.tree.column(col, width=max(max_width, 100))

        # 添加数据行
        for _, row in df.iterrows():
            # 使用实验编号作为item的ID
            item_id = f"item_{row['实验编号']}"
            self.tree.insert("", "end", iid=item_id, values=list(row))

    def save_to_csv(self):
        """保存结果为CSV文件 - 不包含实验编号列"""
        if not hasattr(self, 'design_df'):
            messagebox.showerror("错误", "请先生成实验设计")
            return

        export_dir = "./data/doe_csv/"
        os.makedirs(export_dir, exist_ok=True)
        now_str = datetime.now().strftime("%Y-%m-%d_%H%M%S")
        filename = os.path.join(export_dir, f"doe_{now_str}.csv")

        if filename:
            try:
                # 创建副本以避免修改原始数据框
                df_to_save = self.design_df.copy()

                # 移除实验编号列
                if '实验编号' in df_to_save.columns:
                    df_to_save = df_to_save.drop(columns=['实验编号'])

                # 保存到CSV
                df_to_save.to_csv(filename, index=False, encoding='utf-8-sig')
                messagebox.showinfo("成功", f"文件已保存至: {filename}")
                if self.on_csv_saved:
                    self.on_csv_saved(filename)
            except Exception as e:
                messagebox.showerror("错误", f"保存失败: {str(e)}")

    def import_from_csv(self):
        """导入CSV文件，读取"因子"、"水平范围"和"单位"列"""
        try:
            filename = filedialog.askopenfilename(
                filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")]
            )

            if not filename:
                return  # 用户取消选择

            # 读取CSV文件，尝试不同编码
            try:
                df = pd.read_csv(filename, encoding='utf-8')
            except UnicodeDecodeError:
                try:
                    df = pd.read_csv(filename, encoding='gbk')
                except:
                    df = pd.read_csv(filename, encoding='latin-1')

            # 检查CSV是否为空
            if df.empty:
                messagebox.showerror("错误", "CSV文件为空")
                return

            # 检查是否包含必要的列
            required_columns = ["因子", "水平范围"]
            missing_cols = [col for col in required_columns if col not in df.columns]
            if missing_cols:
                messagebox.showerror("错误", f"CSV文件必须包含以下列: {', '.join(missing_cols)}")
                return

            # 提取因子数据并清除空行
            factors_data = df[required_columns].dropna().reset_index(drop=True)
            num_factors = len(factors_data)

            # 验证因子数量
            if num_factors < 2:
                messagebox.showerror("错误", f"至少需要2个因子，当前为{num_factors}个")
                return
            if num_factors > 10:
                messagebox.showerror("错误", f"最多支持10个因子，当前为{num_factors}个")
                return

            # 固定响应值列名为"响应值"
            self.response_name_var.set("响应值")

            # 更新因子数量并刷新框架
            self.factor_var.set(num_factors)
            self.update_factors_frame()

            # 解析每个因子的信息
            for i in range(num_factors):
                # 获取因子名称
                factor_name = str(factors_data.loc[i, "因子"]).strip()
                if not factor_name:
                    messagebox.showerror("错误", f"第{i + 1}行的因子名称不能为空")
                    return

                # 获取单位（如果CSV中有"单位"列）
                unit = ""
                if "单位" in df.columns and not pd.isna(df.loc[i, "单位"]):
                    unit = str(df.loc[i, "单位"]).strip()

                # 获取水平范围
                level_range = str(factors_data.loc[i, "水平范围"]).strip()
                if not level_range:
                    messagebox.showerror("错误", f"因子 '{factor_name}' 的水平范围不能为空")
                    return

                # 解析水平范围为最小值和最大值
                min_val, max_val = self.parse_level_range(level_range, factor_name)
                if min_val is None or max_val is None:
                    return  # 解析失败，已显示错误消息

                # 检查最小值是否小于最大值
                if min_val >= max_val:
                    messagebox.showerror("错误", f"因子 '{factor_name}' 的最小值必须小于最大值")
                    return

                # 确定水平数（默认2，CSV中有数据则自动计算）
                level_count = 2
                if factor_name in df.columns:
                    factor_values = df[factor_name].dropna().unique()
                    if len(factor_values) >= 2:
                        level_count = len(factor_values)

                # 填充因子信息到界面
                self.factor_entries[i]['name'].set(factor_name)
                self.factor_entries[i]['unit'].set(unit)  # 设置从CSV导入的单位
                self.factor_entries[i]['type'].set("连续型")
                self.factor_entries[i]['level_count'].set(level_count)
                self.factor_entries[i]['min_value'].set(str(round(min_val, 6)))  # 填充最小值
                self.factor_entries[i]['max_value'].set(str(round(max_val, 6)))  # 填充最大值

                # 确保显示连续型输入框
                self._refresh_factor_input_display(i)

            messagebox.showinfo("成功",
                                f"已成功导入 {num_factors} 个因子信息\n"
                                f"因子名称、单位和水平范围已自动填入\n"
                                f"响应值列名固定为: 响应值")

        except Exception as e:
            messagebox.showerror("错误", f"导入CSV失败: {str(e)}")

    def parse_level_range(self, level_range_str, factor_name):
        """解析水平范围字符串为最小值和最大值"""
        # 尝试多种常见的范围分隔符
        separators = [
            ('-', '减号分隔，如10-20'),
            ('~', '波浪线分隔，如10~20'),
            ('—', '长横线分隔，如10—20'),
            ('to', 'to分隔，如10to20'),
            ('到', '到分隔，如10到20'),
            (',', '逗号分隔，如10,20'),
            (' ', '空格分隔，如10 20')
        ]

        for sep, desc in separators:
            if sep in level_range_str:
                parts = [p.strip() for p in level_range_str.split(sep) if p.strip()]
                if len(parts) == 2:
                    try:
                        return float(parts[0]), float(parts[1])
                    except ValueError:
                        messagebox.showerror(
                            "错误",
                            f"因子 '{factor_name}' 的水平范围值不是有效数字\n"
                            f"格式: {desc}"
                        )
                        return None, None

        # 如果没有找到有效的分隔符
        messagebox.showerror(
            "错误",
            f"因子 '{factor_name}' 的水平范围格式不正确\n"
            f"请使用以下格式之一: 10-20, 10~20, 10到20, 10,20 或 10 20"
        )
        return None, None

    def _refresh_factor_input_display(self, index):
        """刷新指定因子的输入框显示"""
        # 触发类型变化事件，确保显示正确的输入框
        for widget in self.factors_frame.winfo_children():
            if hasattr(widget, 'bind') and widget.winfo_class() == 'TCombobox':
                # 找到对应的因子类型下拉框
                if widget.grid_info()['row'] == index + 1 and widget.grid_info()['column'] == 2:
                    widget.event_generate("<<ComboboxSelected>>")
                    break

    def parse_factor_name_and_unit(self, column_name):
        """从列名中解析因子名称和单位（格式如"温度(℃)"）"""
        if '(' in column_name and ')' in column_name:
            name_part, unit_part = column_name.split('(', 1)
            unit = unit_part.rstrip(')').strip()
            return name_part.strip(), unit
        return column_name.strip(), ""

    def determine_factor_type(self, values):
        """确定因子类型（连续型、离散型、分类型）"""
        # 检查是否所有值都是数值
        all_numeric = True
        try:
            [float(v) for v in values]
        except (ValueError, TypeError):
            all_numeric = False

        if not all_numeric:
            return "分类型"

        # 对于数值型，判断是连续型还是离散型
        try:
            numeric_values = [float(v) for v in values]
            std_dev = np.std(numeric_values)

            # 如果只有2个水平，判定为离散型
            if len(numeric_values) == 2:
                return "离散型"

            # 如果标准差为0或非常小，判定为离散型
            if std_dev < 1e-6:
                return "离散型"

            return "连续型"
        except:
            return "分类型"

    def on_double_click(self, event):
        """双击表格单元格时编辑响应值"""
        region = self.tree.identify_region(event.x, event.y)
        if region != "cell":
            return

        # 获取点击的行和列
        item = self.tree.identify_row(event.y)
        column = self.tree.identify_column(event.x)
        col_index = int(column[1:]) - 1  # 转换为0-based索引

        # 只允许编辑响应值列（固定为"响应值"）
        columns = list(self.design_df.columns)
        if col_index < 0 or columns[col_index] != "响应值":
            return

        # 如果已有编辑框，先保存之前的编辑
        if self.editing_cell:
            self.save_edited_cell()

        # 创建编辑框
        x, y, width, height = self.tree.bbox(item, column)
        value = self.tree.item(item, "values")[col_index]

        self.entry = ttk.Entry(self.tree)
        self.entry.place(x=x, y=y, width=width, height=height)
        self.entry.insert(0, value)
        self.entry.focus()

        # 保存当前编辑的单元格信息
        try:
            experiment_id = int(item.split('_')[1]) - 1  # 转换为DataFrame的索引
            self.editing_cell = (item, col_index, experiment_id)
        except:
            # 如果无法解析item ID，使用行号作为后备
            item_index = list(self.tree.get_children()).index(item)
            self.editing_cell = (item, col_index, item_index)

        # 绑定事件
        self.entry.bind("<Return>", self.on_entry_complete)
        self.entry.bind("<Escape>", self.on_entry_cancel)
        self.entry.bind("<FocusOut>", self.on_entry_focus_out)

    def on_entry_complete(self, event):
        """完成编辑，保存值"""
        self.save_edited_cell()

    def on_entry_cancel(self, event):
        """取消编辑，不保存值"""
        self.entry.destroy()
        self.editing_cell = None

    def on_entry_focus_out(self, event):
        """失去焦点时保存值"""
        self.save_edited_cell()

    def save_edited_cell(self):
        """保存编辑的单元格值"""
        if not self.editing_cell:
            return

        item, col_index, row_index = self.editing_cell
        new_value = self.entry.get()

        # 验证输入是否为数值
        try:
            if new_value.strip() == '':
                raise ValueError("响应值不能为空")
            float(new_value)  # 检查是否可以转换为浮点数
        except ValueError as ve:
            messagebox.showerror("输入错误", f"响应值必须为数值: {str(ve)}")
            return

        # 更新表格显示
        values = list(self.tree.item(item, "values"))
        values[col_index] = new_value
        self.tree.item(item, values=values)

        # 更新数据框
        if 0 <= row_index < len(self.design_df):
            self.design_df.loc[row_index, "响应值"] = new_value
        else:
            messagebox.showerror("错误", "行索引超出范围")

        # 清理
        self.entry.destroy()
        self.editing_cell = None
        self.entry = None


if __name__ == "__main__":
    root = tk.Tk()
    app = DOEFullFactorialDesigner(root)
    root.mainloop()
